/**
 *
 */
package de.lgohlke.helper;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.management.ReflectionException;
import javax.xml.bind.ValidationException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <p>ReflectionHelper class.</p>
 *
 * @author Lars Gohlke
 * @version $Id: $
 */
public class ReflectionHelper
{
  private final static Logger log = LoggerFactory.getLogger(ReflectionHelper.class);

  /**
   * mithilfe des Stacktraces die Aufrufermethode finden und ein Method-Objekt zurückgeben
   * <p/>
   * sollte es zwei oder mehr Methoden mit dem gleichen Namen geben wird eine {@link ReflectionException} geworfen
   *
   * @param index
   *          Index der relevaten Aufrufebene (1, wenn direkt hinter Aufruf, <br/>
   *          2, wenn noch eine Methode dazwischen hängt)
   * @throws javax.management.ReflectionException if any.
   * @throws java.lang.ClassNotFoundException if any.
   * @return a {@link java.lang.reflect.Method} object.
   */
  public static Method getMethodFromStackTrace(final int index) throws ReflectionException, ClassNotFoundException
  {

    Method result = null;

    StackTraceElement[] elements = new Throwable().getStackTrace();

    if (index >= elements.length)
    {
      throw new ArrayIndexOutOfBoundsException("der Stacktrace ist kleiner als der index von " + index);
    }

    String callerMethodName = elements[index].getMethodName();

    Class<? extends Object> c = Class.forName(elements[index].getClassName());
    boolean found_method = false;

    for (Method m : c.getDeclaredMethods())
    {
      if (m.getName().equals(callerMethodName))
      {
        if (!found_method)
        {
          // log.info(new XStream().toXML(m) + "\n");
          // log.info(new XStream().toXML(elements[index]));
          // log.info("method: " + m.getName());
          result = m;
          found_method = true;
        }
        else
        {
          Method alreadyFoundMethod = result;
          Method foundAsWellMethod = m;

          /**
           * Interfacemethoden haben den Modifier 4161
           * <p/>
           * 4096 + 64 + 1 = 64 + 1 =^ volatile + public
           * <p/>
           * (4096 als Überlauf) wenn die Rückgabewerte unterschiedlich sind, dann bleibt die InterfaceMethode erhalten
           */

          if ((alreadyFoundMethod.getModifiers() > 4096) || (foundAsWellMethod.getModifiers() > 4096))
          {
            Method interfaceMethod = null;
            Method concreteMethod = null;

            if (alreadyFoundMethod.getModifiers() > 4096)
            {
              interfaceMethod = alreadyFoundMethod;
              concreteMethod = foundAsWellMethod;
            }
            else
            {
              interfaceMethod = foundAsWellMethod;
              concreteMethod = alreadyFoundMethod;
            }

            String message = "eine Methode weicht von ihrem Interface im Rückgabetyp ab: ";
            String message2 = "verlangt das Interface  \"" + interfaceMethod.getReturnType().getCanonicalName() + "\" und die konkrete Methode gibt aber \""
                + concreteMethod.getReturnType().getCanonicalName() + "\" zurück";
            String message3 = "Rückgabewert falsch?";

            throw new ReflectionException(new Exception(), message + callerMethodName + "\n" + message2 + "\n" + message3);
          }
          else
          {
            throw new ReflectionException(new Exception(), "es darf nur eine Methode mit diesem Namen in deiner Klasse geben:" + callerMethodName);
          }
        }
      }
    }

    return result;

  }

  /**
   * die Stelle im Stacktrace heraussuchen, an der die Methode mit dem Namen <i>methodname</i> zuerst aufgerufen wurde
   * (bis dahin ist sozusagen userspace und danach intern)
   * 
   * @param methodname
   * @return
   */
  private static int getFirstCallOfMethodWithNameIntern(final String methodname)
  {
    StackTraceElement[] x = new Exception().getStackTrace();

    int index = -1;
    // TODO sind hier viele unnötige Vergleiche drin?
    for (int i = 0; i < x.length; i++)
    {
      if (x[i].getMethodName().equals(methodname))
      {
        index = i;
      }
    }

    return index;
  }

  /**
   * <p>getFirstCallOfMethodWithName.</p>
   *
   * @param methodname a {@link java.lang.String} object.
   * @return a {@link java.lang.reflect.Method} object.
   * @throws javax.management.ReflectionException if any.
   * @throws java.lang.ClassNotFoundException if any.
   */
  public static Method getFirstCallOfMethodWithName(final String methodname) throws ReflectionException, ClassNotFoundException
  {
    int index = getFirstCallOfMethodWithNameIntern(methodname);
    if (index == -1)
    {
      return null;
    }
    else
    {
      return getMethodFromStackTrace(index);
    }
  }

  /**
   * sucht die Methode aus dem Stacktrace, die vor dem ersten Aufruf der Methode <i>methodname</i> aufgerufen wurde.
   * <p/>
   * (das <i>After</i> im Namen der Methode bezieht sich auf den Stacktrace, der umgekehrt sortiert ist, d.h. die
   * zuletzt aufgerufene Methode ist die mit dem Index '0')
   *
   * @param methodname a {@link java.lang.String} object.
   * @throws javax.management.ReflectionException if any.
   * @throws java.lang.ClassNotFoundException if any.
   * @return a {@link java.lang.reflect.Method} object.
   */
  public static Method getMethodAfterFirstCallOfMethodWithName(final String methodname) throws ReflectionException, ClassNotFoundException
  {
    int index = getFirstCallOfMethodWithNameIntern(methodname);
    if (index == -1)
    {
      return null;
    }
    else
    {
      // hier ist das 'AFTER' aus dem Methodennamen ;)
      return getMethodFromStackTrace(index + 1);
    }
  }

  /**
   * @param baseClass
   * @param otherClass
   * @param alreadyIn
   *          (gegen unendliche Rekursionen)
   * @return
   */
  private static boolean classOf(final Class<? extends Object> baseClass, final Class<? extends Object> otherClass, final Set<Class<? extends Object>> alreadyIn)
  {

    if ((otherClass == null) || (baseClass == null))
    {
      return false;
    }
    else
    {
      if (baseClass.getName().equals(otherClass.getName()))
      {
        return true;
      }
      else
      {
        boolean found = false;
        alreadyIn.add(otherClass);

        if (baseClass.isAnnotation())
        {
          // log.info("StackSize:" + new
          // Exception().getStackTrace().length + " -> " +
          // otherClass);
          // die Interfaces verfolgen und beim ersten Treffer
          // abbrechen
          for (java.lang.annotation.Annotation a : otherClass.getAnnotations())
          {
            // log.info(baseClass + " ? " + a);
            //
            // log.info(" " + a + " contains? " +
            // alreadyIn.contains(a.annotationType()));

            if (!found && !alreadyIn.contains(a.annotationType()))
            {

              found = classOf(baseClass, a.annotationType(), alreadyIn);
            }
          }
        }
        else
        {
          // die Interfaces verfolgen und beim ersten Treffer
          // abbrechen
          for (Class<? extends Object> c : otherClass.getInterfaces())
          {
            if (!found && !alreadyIn.contains(c))
            {
              found = classOf(baseClass, c, alreadyIn);
            }
          }

          if (!found)
          {
            for (Type t : otherClass.getGenericInterfaces())
            {

              if (!found && !alreadyIn.contains(t.getClass()))
              {
                found = classOf(baseClass, t.getClass(), alreadyIn);
              }
            }
          }
        }

        return found || classOf(baseClass, otherClass.getSuperclass(), alreadyIn);
      }
    }
  }

  /**
   * analog zu instanceof für die Eingabeparameter in Form von Klassennamen auch für die Prüfung von
   * Interfaceimplementierungen
   *
   * <pre>
   * classOf(ServiceBean.class, CryptAbstractService.class) : true
   * </pre>
   *
   * , weil CryptAbstractService von SreviceBean abstammt, andersherum wäre es falsch
   * <p/>
   * TODO Test fehlt
   *
   * @param baseClass
   *          : Urklasse
   * @param otherClass
   *          : mögliche Unterklasse
   * @return true, if class of
   */
  public static boolean classOf(final Class<?> baseClass, final Class<?> otherClass)
  {
    Set<Class<? extends Object>> alreadyFound = new HashSet<Class<? extends Object>>();
    return classOf(baseClass, otherClass, alreadyFound);
  }

  /*
	 *
	 */
  /**
   * <p>classOf.</p>
   *
   * @param baseObjectClass a {@link java.lang.Object} object.
   * @param otherObjectClass a {@link java.lang.Object} object.
   * @return a boolean.
   */
  public static boolean classOf(final Object baseObjectClass, final Object otherObjectClass)
  {
    return classOf(baseObjectClass.getClass(), otherObjectClass.getClass());
  }

  /**
   * liefert die Setter in einer Map zurück.
   *
   * @param o
   *          the o
   * @return the setter methods
   */
  public List<Method> getSetterMethods(final Object o)
  {
    List<Method> result = new ArrayList<Method>();

    for (Method method : o.getClass().getMethods())
    {
      String methodName = method.getName();

      if (methodName.startsWith("set"))
      {
        result.add(method);
      }
    }
    return result;
  }

  /**
   * Gets the getter methods.
   *
   * @param o
   *          an object
   * @return list of the getter Methods form object o
   */
  public List<Method> getGetterMethods(final Object o)
  {
    List<Method> result = new ArrayList<Method>();

    for (Method method : o.getClass().getMethods())
    {
      String methodName = method.getName();

      if (methodName.startsWith("get") && !methodName.equals("getClass") && !methodName.equalsIgnoreCase("getid"))
      {
        result.add(method);
      }
    }
    return result;
  }

  /**
   * Compare object I ds.
   *
   * @param obj
   *          the obj
   * @param filter
   *          the filter
   * @return true, if successful
   */
  @Deprecated
  public boolean compareObjectIDs(final Object obj, final Object filter)
  {
    List<Method> objGetter = this.getGetterMethods(obj);
    List<Method> filterGetter = this.getGetterMethods(filter);

    Method objMethod = null;
    Method filterMethod = null;

    Iterator<?> i = objGetter.iterator();

    // getId im object suchen
    while (i.hasNext())
    {
      objMethod = (Method) i.next();
      if (objMethod.getName().equals("getId"))
      {
        break;
      }
    }

    i = filterGetter.iterator();

    // getId im object suchen
    while (i.hasNext())
    {
      filterMethod = (Method) i.next();

      if ((objMethod != null) && objMethod.getName().equals("getId"))
      {
        break;
      }
    }

    try
    {
      if ((filterMethod != null) && (objMethod != null))
      {
        if (filterMethod.invoke(filter) == objMethod.invoke(obj))
        {
          return true;
        }
      }
      else
      {
        return false;
      }

    }
    catch (InvocationTargetException e)
    {
      log.error(e.getMessage(), e);
    }
    catch (IllegalAccessException e)
    {
      log.error(e.getMessage(), e);
    }

    return false;
  }

  /**
   * <p>getAnnotation.</p>
   *
   * @param annotations an array of {@link java.lang.annotation.Annotation} objects.
   * @param annotationClass a {@link java.lang.Class} object.
   * @return a {@link java.lang.Object} object.
   */
  public static Object getAnnotation(final Annotation[] annotations, final Class<?> annotationClass)
  {
    Object result = null;

    for (Annotation a : annotations) // für alle
    {
      if (a.annotationType().equals(annotationClass)) // gefunden?
      {
        result = a;
      }
    }

    return result;
  }

  /**
   * <p>getAnnotation.</p>
   *
   * @param m a {@link java.lang.reflect.Method} object.
   * @param annotationClass a {@link java.lang.Class} object.
   * @return a {@link java.lang.Object} object.
   */
  public static Object getAnnotation(final Method m, final Class<?> annotationClass)
  {
    return getAnnotation(m.getAnnotations(), annotationClass);
  }

  /**
   * <p>getAnnotation.</p>
   *
   * @param c a {@link java.lang.Class} object.
   * @param annotationClass a {@link java.lang.Class} object.
   * @return a {@link java.lang.Object} object.
   */
  public static Object getAnnotation(final Class<?> c, final Class<?> annotationClass)
  {
    return getAnnotation(c.getAnnotations(), annotationClass);
  }

  /**
   * <p>describeObjectModifier.</p>
   *
   * @param member a {@link java.lang.reflect.Member} object.
   */
  public static void describeObjectModifier(final Member member)
  {
    int modifier = member.getModifiers();

    //
    // Return true if the integer argument includes the public modifier,
    // false otherwise.
    //
    if (Modifier.isPublic(modifier))
    {
      log.info(member.getName() + " class modifier is public");
    }

    //
    // Return true if the integer argument includes the protected modifier,
    // false otherwise.
    //
    if (Modifier.isProtected(modifier))
    {
      log.info(member.getName() + " class modifier is protected");
    }

    //
    // Return true if the integer argument includes the private modifier,
    // false otherwise.
    //
    if (Modifier.isPrivate(modifier))
    {
      log.info(member.getName() + " class modifier is private");
    }

    //
    // Return true if the integer argument includes the static modifier,
    // false otherwise.
    //
    if (Modifier.isStatic(modifier))
    {
      log.info(member.getName() + " class modifier is static");
    }

    //
    // Return true if the integer argument includes the final modifier,
    // false otherwise.
    //
    if (Modifier.isFinal(modifier))
    {
      log.info(member.getName() + " class modifier is final");
    }

    //
    // Return true if the integer argument includes the abstract modifier,
    // false otherwise.
    //
    if (Modifier.isAbstract(modifier))
    {
      log.info(member.getName() + " class modifier is abstract");
    }

    if (Modifier.isInterface(modifier))
    {
      log.info(member.getName() + " class modifier is interface");
    }

    if (Modifier.isVolatile(modifier))
    {
      log.info(member.getName() + " class modifier is volatile");
    }
  }

  /**
   * soll den ersten Generischen DatenTyp zurückgeben
   * <p/>
   *
   * <pre>
   * bei List&lt;Integer&gt; list soll Integer rauskommen
   *
   * </pre>
   *
   * im Test ist auch noch ein Beispiel mit generischer Oberklasse
   *
   * @param genericSuperclass a {@link java.lang.reflect.ParameterizedType} object.
   * @return a {@link java.lang.Class} object.
   */
  public static Class<?> getFirstGenericType(final ParameterizedType genericSuperclass)
  {

    // if (classOf(ParameterizedTypeImpl.class, genericSuperclass)){
    ParameterizedType type = genericSuperclass;
    Class<?> myTypeClass = null;

    if (type.getActualTypeArguments().length > 0)
    {
      myTypeClass = (Class<?>) type.getActualTypeArguments()[0];
    }

    return myTypeClass;
    // }
    // else{
    // throw new ClassCastException("must be type of : " + ParameterizedTypeImpl.class);
    // }
  }

  /**
   * überprüft, ob zwei Methoden
   *
   * @param expected
   *          erwartete Methode
   * @param actual
   *          tatsächliche
   * @return a boolean.
   * @throws javax.xml.bind.ValidationException if any.
   */
  public static boolean isSameMethod(final Method expected, final Method actual) throws ValidationException
  {
    class Helper
    {
      /**
       * wenn ein Fehler auftritt wird eine ValidationException geworfen
       * 
       * @param flag
       * @param expected
       * @param actual
       * @throws ValidationException
       */
      public void throwErrorOnFalse(final boolean flag, final Object expected, final Object _actual) throws ValidationException
      {

        if (!flag)
        {
          Object expectedOut = expected;
          Object actualOut = _actual;

          if (expected instanceof Type[])
          {
            expectedOut = "";
            actualOut = "";
            for (Type t : (Type[]) expected)
            {
              expectedOut = t + "\n";
            }
            for (Type t : (Type[]) _actual)
            {
              actualOut = t + "\n";
            }
          }
          // else{
          // if (expected.getClass().isArray()){
          // expectedOut = new XStream().toXML(expected);
          // }
          // }

          throw new ValidationException("\n erwartet : " + expectedOut + "\n bekommen : " + actualOut);
        }
      }
    }
    Helper helper = new Helper();
    boolean result = true;

    if (result)
    {
      result = expected.getReturnType().equals(actual.getReturnType());

      helper.throwErrorOnFalse(result, expected.getReturnType(), actual.getReturnType());
    }

    if (result)
    {
      result = expected.getGenericReturnType().equals(actual.getGenericReturnType());

      helper.throwErrorOnFalse(result, expected.getGenericReturnType(), actual.getGenericReturnType());
    }
    if (result)
    {
      result = (expected.getGenericParameterTypes().length == actual.getGenericParameterTypes().length);

      if (result)
      {
        if (expected.getGenericParameterTypes().length > 0)
        {
          List<Type> expectedTypes = Arrays.asList(expected.getGenericParameterTypes());
          List<Type> actualTypes = Arrays.asList(actual.getGenericParameterTypes());

          result = CollectionHelper.isEqualCollection(expectedTypes, actualTypes);

          helper.throwErrorOnFalse(result, expected.getGenericParameterTypes(), actual.getGenericParameterTypes());
        }
      }
      else
      {
        helper.throwErrorOnFalse(result, expected.getGenericParameterTypes().length, actual.getGenericParameterTypes().length);
      }
    }

    return result;
  }
}
